﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.SessionState;
using CloudPms.Common.CacheProvider;
using ServiceStack.Redis;

namespace CloudPms.Common.SessionServer
{
    public class RedisSessionStateStoreProvider:SessionStateStoreProviderBase
    {
        private static volatile PooledRedisClientManager _pooledRedisClientManager;
        private static readonly object _locker = new object();

        private readonly Func<HttpContext, HttpStaticObjectsCollection> _staticObjectsGetter;
        private volatile int _sessionTimeoutMinutes;
        private volatile string _sessionName;
        private volatile SessionStateSection _sessionSection;

        #region Initialize

        internal RedisSessionStateStoreProvider(Func<HttpContext, HttpStaticObjectsCollection> staticObjectsGetter)
        {
            _staticObjectsGetter = staticObjectsGetter;
        }

        public RedisSessionStateStoreProvider()
        {
            _staticObjectsGetter = SessionStateUtility.GetSessionStaticObjects;
        }

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                name = "Asp_Net_Session";
            }
            _sessionName = name;

            if (_sessionSection == null)
            {
                _sessionSection = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState");
            }

            _sessionTimeoutMinutes = (int)_sessionSection.Timeout.TotalMinutes;
            
            if (_pooledRedisClientManager == null)
            {
                lock (_locker)
                {
                    if (_pooledRedisClientManager == null)
                    {
                        var pm = PooledManager.Instance;

                        _pooledRedisClientManager = pm.GetPooledClientManager();

                    }
                }
            }

            base.Initialize(name, config);
        }

        #endregion

        #region abstract defined

        public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
        {
            return new SessionStateStoreData(new SessionStateItemCollection(), 
                _staticObjectsGetter(context), timeout);
        }

        public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
        {
            var key = GetSessionIdKey(id);
            using (var client = GetClientAndWatch(key))
            {
                var state = new RedisSessionState()
                    {
                        Timeout=timeout,
                        Flags=SessionStateActions.InitializeItem
                    };
                UpdateSessionState(client, key, state);
            }
        }

        public override void Dispose()
        {
            _pooledRedisClientManager.Dispose();
        }

        public override void EndRequest(HttpContext context)
        {
            //throw new NotImplementedException();
        }

        public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetItem(false, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetItem(true, context, id, out locked, out lockAge, out lockId, out actions);
        }

        private SessionStateStoreData GetItem(bool isExclusive, HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            var key = GetSessionIdKey(id);

            locked = false;
            lockAge = TimeSpan.Zero;
            lockId = null;
            actions = SessionStateActions.None;

            using (var client = GetClientAndWatch(key))
            {
                var stateRaw = client.GetAllEntriesFromHashRaw(key);

                RedisSessionState state;
                if (!RedisSessionState.TryParse(stateRaw, out state))
                {
                    client.UnWatch();
                    return null;
                }

                actions = state.Flags;
                var items = actions == SessionStateActions.InitializeItem ? new SessionStateItemCollection() : state.Items;

                if (state.Locked)
                {
                    client.UnWatch();
                    locked = true;
                    lockId = state.LockId;
                    lockAge = DateTime.UtcNow - state.LockDate;
                    return null;
                }

                if (isExclusive)
                {
                    locked = state.Locked = true;
                    state.LockDate = DateTime.UtcNow;
                    lockAge = TimeSpan.Zero;
                    lockId = ++state.LockId;
                }

                state.Flags = SessionStateActions.None;

                using (var t = client.CreateTransaction())
                {
                    t.QueueCommand(c => c.SetRangeInHashRaw(key, state.ToMap()));
                    t.QueueCommand(c => c.ExpireEntryIn(key, TimeSpan.FromMinutes(state.Timeout)));
                    t.Commit();
                }

                return new SessionStateStoreData(items, _staticObjectsGetter(context), state.Timeout);
            }
        }
        
        public override void InitializeRequest(HttpContext context)
        {
            //throw new NotImplementedException();
        }

        public override void ReleaseItemExclusive(HttpContext context, string id, object lockId)
        {
            var key = GetSessionIdKey(id);
            using (var client = GetClientAndWatch(key))
            {
                this.UpdateSessionStateIfLocked(client, key, (int)lockId, state =>
                {
                    state.Locked = false;
                    state.Timeout = _sessionTimeoutMinutes;
                });
            }
        }

        public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item)
        {
            var key = GetSessionIdKey(id);
            using (var client = GetClientAndWatch(key))
            {
                var stateRaw = client.GetAllEntriesFromHashRaw(key);

                using (var transaction = client.CreateTransaction())
                {
                    RedisSessionState state;
                    if (RedisSessionState.TryParse(stateRaw, out state) && state.Locked && state.LockId == (int)lockId)
                    {
                        transaction.QueueCommand(c => c.Remove(key));
                    }

                    transaction.Commit();
                }
            }
        }

        public override void ResetItemTimeout(HttpContext context, string id)
        {
            var key = GetSessionIdKey(id);
            using (var client = GetClientAndWatch(key))
            using (var transaction = client.CreateTransaction())
            {
                transaction.QueueCommand(c => c.ExpireEntryIn(key, TimeSpan.FromMinutes(_sessionTimeoutMinutes)));
                transaction.Commit();
            }
        }

        public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
        {
            var key = GetSessionIdKey(id);
            using (var client = GetClientAndWatch(key))
            {
                if (newItem)
                {
                    var state = new RedisSessionState()
                    {
                        Items = (SessionStateItemCollection)item.Items,
                        Timeout = item.Timeout,
                    };

                    UpdateSessionState(client, key, state);
                }
                else
                {
                    UpdateSessionStateIfLocked(client, key, (int)lockId, state =>
                    {
                        state.Items = (SessionStateItemCollection)item.Items;
                        state.Locked = false;
                        state.Timeout = item.Timeout;
                    });
                }
            }
        }

        public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
        {
            return false;
        }

        #endregion

        #region private Method

        private string GetSessionIdKey(string id)
        {
            return this._sessionName + "/" + id;
        }

        private IRedisClient GetClientAndWatch(string key)
        {
            var client = _pooledRedisClientManager.GetClient();
            client.Watch(key);
            return client;
        }

        private void UpdateSessionStateIfLocked(IRedisClient client, string key, int lockId, Action<RedisSessionState> stateAction)
        {
            var stateRaw = client.GetAllEntriesFromHashRaw(key);
            RedisSessionState state;
            if (RedisSessionState.TryParse(stateRaw, out state) && state.Locked && state.LockId == (int)lockId)
            {
                stateAction(state);
                UpdateSessionState(client, key, state);
            }
        }

        private void UpdateSessionState(IRedisClient client, string key, RedisSessionState state)
        {
            using (var t = client.CreateTransaction())
            {
                t.QueueCommand(c => c.SetRangeInHashRaw(key, state.ToMap()));
                t.QueueCommand(c => c.ExpireEntryIn(key, TimeSpan.FromMinutes(state.Timeout)));
                t.Commit();
            }
        }

        #endregion
    }
}
